/*
 * Created on 2004-01-08
 */
package org.opencube.oms;

import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Properties;

import org.apache.log4j.Logger;
import org.opencube.data.BoundVariable;
import org.opencube.data.CallResult;
import org.opencube.data.DataSource;
import org.opencube.oms.meta.OMSMetaData;
import org.opencube.oms.meta.OMSMetaDataException;
import org.opencube.oms.meta.Scheme;
import org.opencube.oms.meta.SimpleAttribute;

/**
 * @author <a href="mailto:maciek@fingo.pl">FINGO - Maciej Mroczko</a>
 * TODO: comment
 */
public class OMSData
{
	private Logger logger = Logger.getLogger( OMSData.class );
	/**
	 * The database property constant: <code>DataSource</code> implementation class = 'ds_class'
	 */
	public static final String PROPERTY_DATA_SOURCE_CLASS = "ds_class";

	/**
	 * The meta data expire time property constant = 'meta_data.expire_time' 
	 */
	public static final String PROPERTY_META_DATA_EXPIRE_TIME = "meta_data.expire_time";

	/**
	 * The default <code>DataSource</code> implementation: 'org.opencube.data.oracle.Oracle'
	 */
	public static final String DEFAULT_DATA_SOURCE_CLASS = "org.opencube.data.oracle.Oracle";
	
	/**
	 * The default meta data expire time in milis = 1h
	 */
	public static final int DEFAULT_META_DATA_EXPIRE_TIME = 60 * 60 * 1000;
	
	
	
	/* The database_hash_code->meta_data pairs for loaded meta data */
	private static HashMap refreshTimes = new HashMap();
	/* The database_hash_code->time of last load/refresh */
	private static HashMap metaDatas = new HashMap();
	/* The data source which this object is associated with */
	private DataSource connection = null;
	/* Base resource dir */
	private String resourcePath = null;
	/* The user id */
	private int userId = 0;
	/* The meta data expire time */
	private int metaDataExpireTime = DEFAULT_META_DATA_EXPIRE_TIME;
	/* Automaticly refreshing meta data */
	private boolean refreshMetaData = true;
	public OMSData( String resourcePath, String connectionAlias, int userId )
	throws Exception
	{
		this( resourcePath, ResourceHelper.loadDataSourceProperties( resourcePath, connectionAlias ), userId );
	}
	
	public OMSData( String resourcePath, Properties dsProps, int userId,boolean  refreshMetaData)
	throws Exception
	{
		this( resourcePath, getDataSource( dsProps ), userId, refreshMetaData);
	}
	public OMSData( String resourcePath, Properties dsProps, int userId)
	throws Exception
	{
		this( resourcePath, getDataSource( dsProps ), userId, true);
	}
	public OMSData( DataSource dataSource, int userId )
	throws Exception
	{
		this( null, dataSource, userId );
	}
	public OMSData( String resourcePath, DataSource dataSource, int userId )
	throws Exception
	{
		this(resourcePath,dataSource,userId,true);
	}
	public OMSData( String resourcePath, DataSource dataSource, int userId,boolean refreshMetaData )
	throws Exception
	{
		this.userId = userId;
		this.resourcePath = resourcePath;
		this.connection = dataSource;
		this.refreshMetaData=refreshMetaData;
		loadMetaData( false );
	}

	/**
	 * Clones the given element in the database
	 * 
	 * @param element
	 * @param targetElement
	 * @param targetAttribute
	 * @param targetKey
	 * @param recursive
	 * @param withAssociations
	 * 
	 * @return The id of newly cloned element
	 * 
	 * @throws Exception
	 */
	public String cloneElementAppend( OMSElement element, OMSElement targetElement, String targetAttribute, String targetKey, boolean recursive, boolean withAssociations )
	throws Exception
	{
		return DBHelper.cloneElementAppend( getDataSource(), getMetaData(), element, targetElement, targetAttribute, targetKey, recursive, withAssociations, userId );
	}
	
	/**
	 * Clones the given element in the database
	 * 
	 * @param element
	 * @param targetElement
	 * @param targetAttribute
	 * @param targetKey
	 * @param recursive
	 * @param excludedComplexAttributeId 
	 * @param withAssociations
	 * 
	 * @return The id of newly cloned element
	 * 
	 * @throws Exception
	 */
	public String cloneElementAppend( OMSElement element, OMSElement targetElement, String targetAttribute, String targetKey, boolean recursive, boolean withAssociations,String  excludedComplexAttributeId )
	throws Exception
	{
		return DBHelper.cloneElementAppend( getDataSource(), getMetaData(), element, targetElement, targetAttribute, targetKey, recursive, withAssociations, userId, excludedComplexAttributeId );
	}
	
	public String cloneElementBefore( OMSElement element, OMSElement nextElement, String key, boolean recurisve, boolean withAssociations )
	throws Exception
	{
		return DBHelper.cloneElementBefore( getDataSource(), getMetaData(), element, nextElement, key, recurisve, withAssociations, getUserId() );
	}
	
	/**
	 * Removes recursive given element in the database
	 * 
	 * @param element
	 * @param includeElement
	 * @param withAssociations
	 * 
	 * @return The operation result
	 * 
	 * @throws Exception
	 */
	public Integer removeElementRecursive( OMSElement element, boolean includeElement, boolean withAssociations )
	throws Exception
	{
		return DBHelper.removeElementRecursive( getDataSource(), element, includeElement, withAssociations );
	}
	
	/**
	 * Returns the <code>DataSource</code> instance which this object is associated with.
	 *  
	 * @return DataSource
	 */
	public DataSource getDataSource() 
	{
		return this.connection;
	}
	
	/**
	 * Returns new <code>OMSStructure</code> instance.
	 * 
	 * @param whereStr - select criteria
	 * 
	 * @return OMSStructure
	 * 
	 * @throws SQLException
	 * @throws OMSResourceException
	 * @throws OMSMetaDataException
	 */
	public OMSStructure getOMSStructureByWhere( String whereStr ) 
	throws Exception
	{
		return RecordsetConverter.records2omsStructure( DBHelper.selectTreeByWhere( this.connection, whereStr ), getMetaData() );
	}
	/**
	 * Load and merge association in an given <code>OMSStructure</code> instance, using the given resource for select.
	 * 
	 * @param whereStr - select criteria
     * @param placeHolders
     * @param BoundVariable 
     * @param OMSStructure
	 * 
	 * 
	 * @throws SQLException
	 * @throws OMSResourceException
	 * @throws OMSMetaDataException
	 */
	public void getAndMergeOMSAssociationsByResource( String resourceName, HashMap placeHolders, BoundVariable[] bindVariables,OMSStructure structure ) 
	throws Exception
	{
		RecordsetConverter.recordsRelations2omsStructure( executeOnResource( resourceName, placeHolders, bindVariables ).getRecordSets()[ 0 ], structure,getMetaData() );
	}
	/**
	 * Returns new <code>OMSStructure</code> instance, using the given resource for select.
	 * 
	 * @param whereStr - select criteria
	 * 
	 * @return OMSStructure
	 * 
	 * @throws SQLException
	 * @throws OMSResourceException
	 * @throws OMSMetaDataException
	 */
	public OMSStructure getOMSStructureByResource( String resourceName, HashMap placeHolders, BoundVariable[] bindVariables ) 
	throws Exception
	{
		return RecordsetConverter.records2omsStructure( executeOnResource( resourceName, placeHolders, bindVariables ).getRecordSets()[ 0 ], getMetaData() );
	}

	/**
	 * Returns new <code>OMSStructure</code> instance, using the given statement for select.
	 * 
	 */
	public OMSStructure getOMSStructureByStatement( String statement, HashMap placeHolders, BoundVariable[] boundVariables ) 
	throws Exception
	{
		return RecordsetConverter.records2omsStructure( DBHelper.executeStatement( getDataSource(), placeHolders, boundVariables, statement, null, null ).getRecordSets()[ 0 ], getMetaData() );
	}

	/**
	 * Returns new <code>OMSStructure</code> instance, using the given statement for select.
	 * 
	 */
	public OMSStructure getOMSStructureByStatement( String statement, HashMap placeHolders, BoundVariable[] boundVariables, String namespace, String resourceKey ) 
	throws Exception
	{
		return RecordsetConverter.records2omsStructure( DBHelper.executeStatement( getDataSource(), placeHolders, boundVariables, statement, namespace, resourceKey ).getRecordSets()[ 0 ], getMetaData() );
	}

	/**
	 * Saves the given element tree into the database. 
	 * 
	 * @param tree - tree to save
	 * 
	 * @return OMSStructure - tree updated while saving
	 */
	public OMSStructure saveOMSStructure( OMSStructure tree ) 
	throws Exception
	{
		logger.debug( "Start saving oms structure" );
		
		LinkedList compositions2Delete = new LinkedList();
		LinkedList associations2Delete = new LinkedList();
		LinkedList simpleValues2Delete = new LinkedList();
		LinkedList clobValues2Delete = new LinkedList();
		LinkedList blobValues2Delete = new LinkedList();
		LinkedList uniqueValues2Delete = new LinkedList();
		LinkedList elements2Delete = new LinkedList();
		LinkedList elements2Add = new LinkedList();
		LinkedList simpleValues2Add = new LinkedList();
		LinkedList clobValues2Add = new LinkedList();
		LinkedList blobValues2Add = new LinkedList();
		LinkedList uniqueValues2Add = new LinkedList();
		LinkedList elements2Update = new LinkedList();
		LinkedList simpleValues2Update = new LinkedList();
		LinkedList clobValues2Update = new LinkedList();
		LinkedList blobValues2Update = new LinkedList();
		LinkedList uniqueValues2Update = new LinkedList();
		LinkedList compositions2Add = new LinkedList();
		LinkedList associations2Add = new LinkedList();
		LinkedList compositions2Update = new LinkedList();
		LinkedList associations2Update = new LinkedList();
		
		HashMap nodesStates = new HashMap();
		
		
		logger.debug( "Get node for save..." );
		OMSNode[] nodes = getNodes2Save( tree );
		
		logger.debug( "Build lists" );
		buildLists( 
			nodes, 
			nodesStates,
			compositions2Delete,
			associations2Delete,
			simpleValues2Delete,
			clobValues2Delete,
			blobValues2Delete,
			uniqueValues2Delete,
			elements2Delete,
			elements2Add,
			simpleValues2Add,
			clobValues2Add,
			blobValues2Add,
			uniqueValues2Add,
			elements2Update,
			simpleValues2Update,
			clobValues2Update,
			blobValues2Update,
			uniqueValues2Update,
			compositions2Add,
			associations2Add,
			compositions2Update,
			associations2Update 
		);
		logger.debug( "Supports batching: " + getDataSource().supportsBatching() );
		logger.debug( "Use batching: " + ( getDataSource().supportsBatching() && getDataSource().useBatchingWhenSupported() ) );
		DBHelper helper = getDataSource().supportsBatching() && getDataSource().useBatchingWhenSupported() ? new BatchHelper() : new DBHelper(); 
	//	DBHelper helper = new DBHelper();
		try
		{
			OMSElement[] rightsElements = tree.getElements();
			OMSElement[] removedElements = tree.getRemovedElements();
			// delete rights
			int processed = processRights( rightsElements, OMSNode.STATE_REMOVED, true, nodesStates );
			processed += processRights( removedElements, OMSNode.STATE_REMOVED, true, nodesStates );
			logger.debug( "Deleted rights: " + processed );
			// delete compositions
			logger.debug( "Deleted compositions: " + helper.removeCompositions( getMetaData(), getDataSource(), (Relation[]) compositions2Delete.toArray(new Relation[compositions2Delete.size()]), getUserId() ) );
			// delete associations
			logger.debug( "Deleted associations: " + helper.removeAssociations( getMetaData(), getDataSource(), (Relation[]) associations2Delete.toArray(new Relation[associations2Delete.size()]), getUserId() ) );
			// delete simple values
			logger.debug( "Deleted simple values: " + helper.removeSimpleValues(getMetaData(), getDataSource(), (Value[]) simpleValues2Delete.toArray(new Value[simpleValues2Delete.size()]), getUserId() ) );
			// delete clob values
			logger.debug( "Deleted clob values: " + helper.removeClobValues(getMetaData(), getDataSource(), (Value[]) clobValues2Delete.toArray(new Value[clobValues2Delete.size()]), getUserId() ) );
			// delete blob values
			logger.debug( "Deleted blob values: " + helper.removeBlobValues(getMetaData(), getDataSource(), (Value[]) blobValues2Delete.toArray(new Value[blobValues2Delete.size()]), getUserId() ) );
			// delete unique values
			logger.debug( "Deleted unique values: " + helper.removeUniqueValues(getMetaData(), getDataSource(), (Value[]) uniqueValues2Delete.toArray(new Value[uniqueValues2Delete.size()]), getUserId() ) );
			// delete elements
			logger.debug( "Deleted elements: " + helper.removeElements(getMetaData(), getDataSource(), (OMSElement[]) elements2Delete.toArray(new OMSElement[elements2Delete.size()]), getUserId() ) );
			// add elements
			logger.debug( "Inserted elements: " + helper.createElements(getMetaData(), getDataSource(), (OMSElement[]) elements2Add.toArray(new OMSElement[elements2Add.size()]), getUserId() ) );
			// add simple values
			logger.debug( "Inserted simple values: " + helper.createSimpleValues(getMetaData(), getDataSource(), (Value[]) simpleValues2Add.toArray(new Value[simpleValues2Add.size()]), getUserId() ) );
			// add clob values
			logger.debug( "Inserted clob values: " + helper.createClobValues(getMetaData(), getDataSource(), (Value[]) clobValues2Add.toArray(new Value[clobValues2Add.size()]), getUserId() ) );
			// add blob values
			logger.debug( "Inserted blob values: " + helper.createBlobValues(getMetaData(), getDataSource(), (Value[]) blobValues2Add.toArray(new Value[blobValues2Add.size()]), getUserId() ) );
			// add unique values
			logger.debug( "Inserted unique values: " + helper.createUniqueValues(getMetaData(), getDataSource(), (Value[]) uniqueValues2Add.toArray(new Value[uniqueValues2Add.size()]), getUserId() ) );
			// update elements
			logger.debug( "Updated elements: " + helper.updateElements(getMetaData(), getDataSource(), (OMSElement[]) elements2Update.toArray(new OMSElement[elements2Update.size()]), getUserId() ) );
			// update simple values
			logger.debug( "Updated simple values: " + helper.updateSimpleValues(getMetaData(), getDataSource(), (Value[]) simpleValues2Update.toArray(new Value[simpleValues2Update.size()]), getUserId() ) );
			// update clob values
			logger.debug( "Updated clob values: " + helper.updateClobValues(getMetaData(), getDataSource(), (Value[]) clobValues2Update.toArray(new Value[clobValues2Update.size()]), getUserId() ) );
			// update blob values
			logger.debug( "Updated blob values: " + helper.updateBlobValues(getMetaData(), getDataSource(), (Value[]) blobValues2Update.toArray(new Value[blobValues2Update.size()]), getUserId() ) );
			// update unique values
			logger.debug( "Updated unique values: " + helper.updateUniqueValues(getMetaData(), getDataSource(), (Value[]) uniqueValues2Update.toArray(new Value[uniqueValues2Update.size()]), getUserId() ) );
			// add compositions
			logger.debug( "Inserted compositions: " + helper.createCompositions(getMetaData(), getDataSource(), (Relation[]) compositions2Add.toArray(new Relation[compositions2Add.size()]), getUserId() ) );
			// add associations
			logger.debug( "Inserted associations: " + helper.createAssociations(getMetaData(), getDataSource(), (Relation[]) associations2Add.toArray(new Relation[associations2Add.size()]), getUserId() ) );
			// insert rights
			logger.debug( "Insert rights: " + processRights( rightsElements, OMSNode.STATE_NEW, false, nodesStates ) );
			// update compositions
			logger.debug( "Updated compositions: " + helper.updateCompositions(getMetaData(), getDataSource(), (Relation[]) compositions2Update.toArray(new Relation[compositions2Update.size()]), getUserId() ) );
			// update associations
			logger.debug( "Updated associations: " + helper.updateAssociations(getMetaData(), getDataSource(), (Relation[]) associations2Update.toArray(new Relation[associations2Update.size()]), getUserId() ) );
			// update rights
			logger.debug( "Updated rights: " + processRights( rightsElements, OMSNode.STATE_UPDATED, false, nodesStates ) );
			// update tree state
			// remove deleted elements and relations
			tree.cleanRemoved();
			// remove rights from non-deleted elements
			for (int i = 0; rightsElements != null && i < rightsElements.length; i++)
			{
				rightsElements[ i ].removedRights.clear();
			}
			// remove values from non-deleted elements
			ArrayList values2Delete = new ArrayList();
			values2Delete.addAll( simpleValues2Delete );
			values2Delete.addAll( clobValues2Delete );
			values2Delete.addAll( blobValues2Delete );
			values2Delete.addAll( uniqueValues2Delete );
			for (int i = 0; values2Delete != null && i < values2Delete.size(); i++)
			{
				Value value = (Value) values2Delete.get( i );
				if( !value.getElement().isRemoved() )
				{
					value.getElement().removeValue( value.getSimpleAttribute(), value.getDatatypeEnumeration() );
				}
			}

			logger.debug( "Tree state updated" );
			logger.debug( "Oms structure saved" );
	
			return tree;
		}
		catch( Exception e )
		{
			logger.error( "Error while tree saving: " + e.getMessage(), e );
			Iterator it = nodesStates.keySet().iterator();
			while( it.hasNext() )
			{
				OMSNode n = (OMSNode) it.next();
				Integer state = (Integer) nodesStates.get( n );
				n.setState( state.intValue() );
			}
			logger.info( "Tree state restored" );
			throw e;
		}
		
		
	}
	
	private int processRights( OMSElement[] elements, int state, boolean reverse, HashMap nodesStates ) throws Exception
	{
		int result = 0;
		int index = reverse ? elements.length - 1 : 0;
		while( index < elements.length && index >= 0 )
		{
			OMSRights[] rights = state == OMSNode.STATE_REMOVED ? elements[ index ].getRemovedRightsArray() : elements[ index ].getRightsArray();
			for (int i = 0; i < rights.length; i++)
			{
				if( state == rights[ i ].getState() )
				{
					nodesStates.put( rights[ i ], new Integer( rights[ i ].getState() ) );
					DBHelper.saveRights( this.connection, rights[ i ], elements[ index ], this.userId );
					result++;
				}
			}
			if( reverse )
			{
				index--;
			}
			else
			{
				index++;
			}
		}
		return result;
	}
	/**
	 * Binds the given variables and executes the statement.
	 * 
	 * @param resourceFile - the file with statement to execute
	 * @param placeHolders - the token to replace withing the statement
	 * @param bindVariables - variables to bind
	 * 
	 * @return HashMap[][] - one array for one output cursor from the statement - single 
	 * array contains records as column_name->value maps.
	 */
	public CallResult executeOnResource( String resourceFile, HashMap placeHolders, BoundVariable[] bindVariables )
	throws Exception
	{
	    File file = new File(resourceFile);
	    if(file.isAbsolute())
	    {
	        String absPath = file.getParent();
	        String resFile = file.getName();
	        return DBHelper.executeOnApplicationResource( this.connection, placeHolders, bindVariables, absPath, resFile );
	    }
	    else
	    {
	        return DBHelper.executeOnApplicationResource( this.connection, placeHolders, bindVariables, this.resourcePath, resourceFile );
	    }
	}


	
	/**
	 * Returns the <code>OMSMetaData</code> instance associated with this object.
	 * If the meta data is older than assumed, refreshes it.
	 * 
	 * @return OMSMetaData - the meta data
	 */
	public OMSMetaData getMetaData() throws Exception
	{
		loadMetaData( false );
		return (OMSMetaData) metaDatas.get( getDatabaseKey() );
	}
	
	/**
	 * Performs meta data refresh, independing on it's age 
	 */
	public void reloadMetaData() throws Exception
	{
		loadMetaData( true );
	}
	
	/* 
	 * Loads the meta data if it's not loaded yet, if it's older than assumed or if if the given 
	 * flag is set 
	 */
	private void loadMetaData( boolean reloadAlways ) throws Exception
	{
		synchronized( metaDatas )
		{
			if( reloadAlways || needsRefresh() )
			{
				logger.info( "Load meta data" );
				refreshTimes.put( getDatabaseKey(), new Long( System.currentTimeMillis() ) );
				metaDatas.put( getDatabaseKey(), RecordsetConverter.records2metaData( DBHelper.selectFullMetaData( this.connection ) ) );
			}
		}
	}
	
	/* Return the key for the current database */
	private Object getDatabaseKey()
	{
		return new Integer( this.connection.hashCode() );
	}
	
	/**
	 * Returns all new, updated or removed nodes from the given structure
	 * 
	 * @param omsStructure - structure to retrieve nodes
	 * 
	 * @return OMSNode[] - all nodes to be saved
	 */
	private OMSNode[] getNodes2Save( OMSStructure omsStructure )
	{
		OMSElement[] elements = omsStructure.getElements();
		ArrayList nodes = new ArrayList();
		for( int i = 0; elements != null && i < elements.length; i++ )
		{
			if( elements[ i ].getState() != OMSNode.STATE_NORMAL )
			{
				nodes.add( elements[ i ] );
			}
			Value[] values = elements[ i ].getValues();
			for( int j = 0; values != null && j < values.length; j++ )
			{
				if( values[ j ].getState() != OMSNode.STATE_NORMAL )
				{
					nodes.add( values[ j ] );
				}
			}
			Relation[] relations = elements[ i ].getRelations();
			for( int j = 0; relations != null && j < relations.length; j++ )
			{
				if( relations[ j ].getState() != OMSNode.STATE_NORMAL )
				{
					nodes.add( relations[ j ] );
				}
			}
		}
		Relation[] rootRelations = omsStructure.getRootElement().getRelations();
		for( int i = 0; rootRelations != null && i < rootRelations.length; i++ )
		{
			if( rootRelations[ i ].getState() != OMSNode.STATE_NORMAL )
			{
				nodes.add( rootRelations[ i ] );
			}
		}

		// add all removed elements
		nodes.addAll( Arrays.asList( omsStructure.getRemovedElements() ) );
		// add all removed relations
		nodes.addAll( Arrays.asList( omsStructure.getRemovedRelations() ) );
		
		Collections.sort( nodes, new NodeComparator() );
		return (OMSNode[]) nodes.toArray( new OMSNode[ nodes.size() ] );
	}
	
	/**
	 * Returns new instance of <code>Database</code> implementation, with the properties under given
	 * section ( alias ) from the specified path as the constructor parameter. 
	 * @param path
	 * @param alias
	 * @return
	 */
	protected static DataSource getDataSource( Properties dsProps ) 
	throws Exception
	{
		String dsClassName = dsProps.getProperty( PROPERTY_DATA_SOURCE_CLASS );
		if( dsClassName == null )
		{
			dsClassName = DEFAULT_DATA_SOURCE_CLASS;
		}
		Class dsClass = Class.forName( dsClassName );
		
		DataSource ds = ( DataSource ) dsClass.newInstance();
		ds.connect( dsProps );
		return ds;
	}
	
	/* Separates the given nodes into typed list */
	private void buildLists( 
		OMSNode[] nodes, 
		HashMap nodesStates,
		LinkedList compositions2Delete,
		LinkedList associations2Delete,
		LinkedList simpleValues2Delete,
		LinkedList clobValues2Delete,
		LinkedList blobValues2Delete,
		LinkedList uniqueValues2Delete,
		LinkedList elements2Delete,
		LinkedList elements2Add,
		LinkedList simpleValues2Add,
		LinkedList clobValues2Add,
		LinkedList blobValues2Add,
		LinkedList uniqueValues2Add,
		LinkedList elements2Update,
		LinkedList simpleValues2Update,
		LinkedList clobValues2Update,
		LinkedList blobValues2Update,
		LinkedList uniqueValues2Update,
		LinkedList compositions2Add,
		LinkedList associations2Add,
		LinkedList compositions2Update,
		LinkedList associations2Update
	)
	throws Exception
	{
		for( int i = 0; nodes != null && i < nodes.length; i++ )
		{
			OMSNode node = nodes[ i ];
			nodesStates.put( node, new Integer( node.getState() ) );
			// set the status values
			node.modifier = this.userId + "";
			node.modifyingDate = new Date();
			if( node.isNew() )
			{
				node.creater= this.userId + "";
				node.creatingDate = new Date();
			}
			if( node instanceof OMSElement )
			{
				if( node.isRemoved() )
				{
					elements2Delete.add( node );
				}
				else if( node.isNew() )
				{
					elements2Add.add( node );
				}
				else if( node.isUpdated() )
				{
					elements2Update.add( node );
				}
			}
			else if( node instanceof Value )
			{
				Value value = (Value) node;
				
				boolean simple = isSimple( value );
				boolean clob = !simple && isClob( value ); 
				boolean blob = !simple && !clob && isBlob( value );
				boolean unique = !simple && !clob && !blob && isUnique( value );
				if( node.isRemoved() )
				{
					if( blob )
					{
						blobValues2Delete.add( value );
					}
					else if( clob )
					{
						clobValues2Delete.add( value );
					}
					else if( unique )
					{
						uniqueValues2Delete.add( value );
					}
					else
					{
						simpleValues2Delete.add( node );
					}
				}
				else if( node.isNew() )
				{
					if( blob )
					{
						blobValues2Add.add( value );
					}
					else if( clob )
					{
						clobValues2Add.add( value );
					}
					else if( unique )
					{
						uniqueValues2Add.add( value );
					}
					else
					{
						simpleValues2Add.add( node );
					}
				}
				else if( node.isUpdated() )
				{
					if( blob )
					{
						blobValues2Update.add( value );
					}
					else if( clob )
					{
						clobValues2Update.add( value );
					}
					else if( unique )
					{
						uniqueValues2Update.add( value );
					}
					else
					{
						simpleValues2Update.add( node );
					}
				}
			}
			else if( node instanceof Relation )
			{
				Relation relation = (Relation) node;
				if( relation.isAssociation() )
				{
					if( node.isRemoved() )
					{
						associations2Delete.add( node );
					}
					else if( node.isNew() )
					{
						associations2Add.add( node );
					}
					else if( node.isUpdated() || node.getState()==OMSNode.STATE_KEYUPDATED )
					{
						associations2Update.add( node );
					}
				}
				else if( relation.isComposition() )
				{
					if( node.isRemoved() )
					{
						compositions2Delete.add( 0, node );
					}
					else if( node.isNew() )
					{
						compositions2Add.add( node );
					}
					else if( node.isUpdated() || node.getState()==OMSNode.STATE_KEYUPDATED)
					{
						compositions2Update.add( node );
					}
				}
			}
		}
	}
	
	private boolean isSimple( Value value )
	throws Exception
	{
		Scheme scheme = getMetaData().getScheme( value.getElement().getNamespacePath(), value.getElement().getSchemeName() );
		if (scheme ==null) {
			throw new Exception ("could not find scheme with name: "+value.getElement().getNamespacePath()+"/"+value.getElement().getSchemeName()+" element: "+value.getElement().getDisplayName());
		}
		SimpleAttribute sat = getMetaData().getSimpleAttributeFromHierarchy( scheme, value.getSimpleAttribute() );
		return sat.getDatatype().isSimple();
	}

	private boolean isBlob( Value value )
	throws Exception
	{
		Scheme scheme = getMetaData().getScheme( value.getElement().getNamespacePath(), value.getElement().getSchemeName() );
		SimpleAttribute sat = getMetaData().getSimpleAttributeFromHierarchy( scheme, value.getSimpleAttribute() );
		return sat.getDatatype().isBlob();
	}
	
	private boolean isClob( Value value )
	throws Exception
	{
		Scheme scheme = getMetaData().getScheme( value.getElement().getNamespacePath(), value.getElement().getSchemeName() );
		SimpleAttribute sat = getMetaData().getSimpleAttributeFromHierarchy( scheme, value.getSimpleAttribute() );
		return sat.getDatatype().isClob();
	}

	private boolean isUnique( Value value )
	throws Exception
	{
		
		Scheme scheme = getMetaData().getScheme( value.getElement().getNamespacePath(), value.getElement().getSchemeName() );
		SimpleAttribute sat = getMetaData().getSimpleAttributeFromHierarchy( scheme, value.getSimpleAttribute() );
		return sat.getDatatype().getIsUnique();
	}
	
	/**
	 * Returns the user id
	 * 
	 * @return The user id
	 */
	public int getUserId()
	{
		return this.userId;
	}
	
	private boolean needsRefresh()
	{
		Long lastRefresh = (Long) refreshTimes.get( getDatabaseKey() );
		if (this.refreshMetaData==true || lastRefresh==null) { 
			return lastRefresh == null || ( System.currentTimeMillis() - lastRefresh.longValue() ) > this.metaDataExpireTime;
		}else {
			return false;
		}
	}

	/**
	 * Returns the unique key for locking
	 * 
	 * @return The unique key for locking
	 * 
	 * @throws Exception
	 */
	public String getLockKey()
	throws Exception
	{
		return DBHelper.getLockerKey( getDataSource() );
	}
	
	/**
	 * Test if the given element is locked in the database
	 * 
	 * @param element element to test
	 * 
	 * @return <code>true</code> if the given element is locked in the database
	 * 
	 * @throws Exception
	 */
	public boolean isElementLocked( OMSElement element )
	throws Exception
	{
		return DBHelper.isElementLocked( getDataSource(), element );
	}

	/**
	 * Removes lock with given id from the database
	 * 
	 * @param id
	 * 
	 * @throws Exception
	 */
	public void unlockById( String id )
	throws Exception
	{
		DBHelper.removeLockById( getDataSource(), id );
	}

	/**
	 * Removes all locks with the given key from the database
	 * 
	 * @param key
	 * 
	 * @throws Exception
	 */
	public void unlockByKey( String key )
	throws Exception
	{
		DBHelper.removeLockByKey( getDataSource(), key );
	}
    /**
     * Update an Lock for an element and the lock key
     * 
     * @param element
     * @param key
     * @param ip
     * @param interval
     * 
     * @return The id of the newly created lock
     * 
     * @throws Exception
     */
    public void updateLockById( String lckId)
    throws Exception
    {
        DBHelper.updateLock( getDataSource(), lckId);
    }
    
	/**
	 * Creates a lock on the given element in the database
	 * 
	 * @param element
	 * @param key
	 * @param ip
	 * @param interval
	 * 
	 * @return The id of the newly created lock
	 * 
	 * @throws Exception
	 */
	public String lockElement( OMSElement element, String key, String ip, int interval )
	throws Exception
	{
		return DBHelper.lockElement( getDataSource(), element, key, ip, interval, userId );
	}
	
	/**
	 * Creates lock in the database on all the elements from the given structure
	 * using the same key.
	 * 
	 * @param tree
	 * @param key
	 * @param ip
	 * @param interval
	 * 
	 * @throws Exception
	 */
	public void lockStructure( OMSStructure structure, String key, String ip, int interval )
	throws Exception
	{
		DBHelper.lockStructure( getDataSource(), structure, key, ip, interval, userId );
	}

	/**
	 * Load rights for given element
	 * Reset any previous made changes in element rights
	 * Loaded are only rights granted to elements present in the same structure as grantedFor
	 * @param grantedFor OMSElement to load the rights for
	 * @return int number of rights found 
	 * @throws Exception
	 */
	public int loadRights( OMSElement grantedFor ) throws Exception
	{
		OMSRights[] rights = RecordsetConverter.records2omsRights( DBHelper.loadRights( this.connection, grantedFor, null ), grantedFor );
		grantedFor.resetRights( rights );
		return rights.length;
	}

	/**
	 * Load rights for given element
	 * Reset any previous made changes in element rights
	 * Load all needed owner elements too
	 * @param grantedFor OMSElement to load the rights for
	 * @return int number of rights found 
	 * @throws Exception
	 */
	public int loadRightsWithOwners( OMSElement grantedFor ) throws Exception
	{
		OMSStructure structure = RecordsetConverter.records2omsStructure( DBHelper.selectRightsOwners( this.connection, grantedFor ), getMetaData() );
		grantedFor.getOMSStructure().merge( structure );
		return loadRights( grantedFor );
	}

	/**
	 * Load rights for given element granted to particular element (rights owner)
	 * Add loaded rights to existing ones or does nothing if rights not found
	 * @param grantedFor OMSElement to load the rights for 
	 * @param grantedTo OMSElement having the rights; have to be in the same OMSStructure with grantedFor
	 * @return boolean true if rights loaded, else if not
	 * @throws Exception
	 */
	public boolean loadRights( OMSElement grantedFor, OMSElement grantedTo ) throws Exception
	{
		OMSRights[] rights = RecordsetConverter.records2omsRights( DBHelper.loadRights( this.connection, grantedFor, grantedTo ), grantedFor );
		if( rights.length == 1 )
		{
			grantedFor.putRights( rights[ 0 ] );
			return true;
		}
		return false;
	}

	/**
	 * Load rights for given element
	 * Reset any previous made changes in element rights
	 * Loaded are only rights granted to elements present in the same structure as grantedFor
	 * @param grantedFor OMSElement[] to load the rights for
	 * @return int number of rights found 
	 * @throws Exception
	 */
	public int loadRights( OMSElement[] grantedFor ) throws Exception
	{
		OMSRights[] rights = RecordsetConverter.records2omsRights( DBHelper.loadRights( this.connection, grantedFor, null ), grantedFor );
		for( int i = 0; i < grantedFor.length; i++ )
		{
		    grantedFor[i].resetRights();
		}
		for( int i = 0; i < rights.length; i++ )
		{
		    OMSElement element = rights[i].getGrantedFor();
		    element.putRights( rights[i] );
            rights[i].setState( OMSNode.STATE_NORMAL);
		}
		return rights.length;
	}

	/**
	 * Load rights for given element
	 * Reset any previous made changes in element rights
	 * Load all needed owner elements too
	 * @param grantedFor OMSElement[] to load the rights for
	 * @return int number of rights found 
	 * @throws Exception
	 */
	public int loadRightsWithOwners( OMSElement[] grantedFor ) throws Exception
	{
		OMSStructure structure = RecordsetConverter.records2omsStructure( DBHelper.selectRightsOwners( this.connection, grantedFor ), getMetaData() );
		grantedFor[0].getOMSStructure().merge( structure );
		return loadRights( grantedFor );
	}

	/**
	 * Load rights for given element granted to particular element (rights owner)
	 * Add loaded rights to existing ones or does nothing if rights not found
	 * @param grantedFor OMSElement[] to load the rights for 
	 * @param grantedTo OMSElement[] having the rights; have to be in the same OMSStructure with grantedFor
	 * @return boolean true if rights loaded, else if not
	 * @throws Exception
	 */
	public boolean loadRights( OMSElement[] grantedFor, OMSElement[] grantedTo ) throws Exception
	{
		OMSRights[] rights = RecordsetConverter.records2omsRights( DBHelper.loadRights( this.connection, grantedFor, grantedTo ), grantedFor );
		if( rights.length > 0 )
		{
			for( int i = 0; i < rights.length; i++ )
			{
			    OMSElement element = rights[i].getGrantedFor();
			    element.putRights( rights[i] );
			}
			return true;
		}
		return false;
	}
    
    
    
	public void setUserId(int userId) {
		this.userId = userId;
	}
    
	public CallResult executeCall( String statement, HashMap<String, String> placeHolders, BoundVariable[] boundVariables )
	throws Exception
	{
		return executeCall( statement, placeHolders, boundVariables, null, null );
	}
	public CallResult executeCall( String statement, HashMap<String, String> placeHolders, BoundVariable[] boundVariables, String namespace, String resourceKey )
	throws Exception
	{
		return DBHelper.executeStatement( getDataSource(), placeHolders, boundVariables, statement, namespace, resourceKey );
	}
}
